<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>方法调用树可视化</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sql-formatter@12.2.4/dist/sql-formatter.min.js"></script>

    <!-- 配置Tailwind自定义样式 -->
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        sql: '#8B5CF6',
                        error: '#EF4444',
                        neutral: '#64748B'
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                        mono: ['Fira Code', 'monospace']
                    }
                }
            }
        }
    </script>

    <style type="text/tailwindcss">
        @layer utilities {
            .node-hover {
                @apply transition-all duration-200 scale-105 shadow-lg;
            }
            .tree-container {
                @apply overflow-auto bg-gray-50 rounded-lg border border-gray-200;
            }
            .details-panel {
                @apply bg-white rounded-lg border border-gray-200 shadow-md p-4 transition-all duration-300;
            }
            .link-highlight {
                @apply stroke-primary stroke-[2px];
            }
        }
    </style>
</head>
<body class="bg-gray-100 min-h-screen font-sans text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-7xl">
    <!-- 页面标题 -->
    <header class="mb-8">
        <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-800 mb-2">方法调用树可视化</h1>
        <p class="text-gray-600">展示方法调用关系及SQL执行详情，点击节点可展开/折叠，悬停查看详细信息</p>
    </header>

    <!-- 控制面板 -->
    <div class="flex flex-wrap gap-4 mb-6 items-center">
        <div class="flex items-center gap-2">
            <button id="zoomIn" class="bg-primary hover:bg-primary/90 text-white px-3 py-1.5 rounded-md flex items-center gap-1 transition-colors">
                <i class="fa fa-search-plus"></i> 放大
            </button>
            <button id="zoomOut" class="bg-primary hover:bg-primary/90 text-white px-3 py-1.5 rounded-md flex items-center gap-1 transition-colors">
                <i class="fa fa-search-minus"></i> 缩小
            </button>
            <button id="resetView" class="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1.5 rounded-md flex items-center gap-1 transition-colors">
                <i class="fa fa-refresh"></i> 重置视图
            </button>
        </div>

        <div class="flex items-center gap-2 ml-auto">
            <div class="flex items-center gap-1">
                <span class="w-3 h-3 rounded-full bg-primary inline-block"></span>
                <span class="text-sm">方法节点</span>
            </div>
            <div class="flex items-center gap-1">
                <span class="w-3 h-3 rounded-full bg-sql inline-block"></span>
                <span class="text-sm">SQL节点</span>
            </div>
            <div class="flex items-center gap-1">
                <span class="w-3 h-3 rounded-full bg-error inline-block"></span>
                <span class="text-sm">错误节点</span>
            </div>
            <div class="flex items-center gap-1">
                <span class="w-3 h-3 bg-gray-400 inline-block"></span>
                <span class="text-sm">连接线</span>
            </div>
        </div>
    </div>

    <!-- 主内容区 -->
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <!-- 可视化区域 -->
        <div class="lg:col-span-2">
            <div class="tree-container h-[70vh] relative">
                <svg id="treeSvg" class="w-full h-full"></svg>
                <div id="loadingIndicator" class="absolute inset-0 flex items-center justify-center bg-white/80 z-10">
                    <div class="flex flex-col items-center">
                        <i class="fa fa-circle-o-notch fa-spin text-4xl text-primary mb-3"></i>
                        <p>加载调用树数据中...</p>
                    </div>
                </div>
                <div id="errorMessage" class="absolute inset-0 flex items-center justify-center bg-white/80 z-10 hidden">
                    <div class="flex flex-col items-center text-error p-4">
                        <i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
                        <p>加载调用树失败，请检查数据格式</p>
                    </div>
                </div>
            </div>
        </div>

        <!-- 详情面板 -->
        <div>
            <div id="nodeDetails" class="details-panel h-[70vh] overflow-auto opacity-0 pointer-events-none">
                <h2 class="text-xl font-semibold mb-4 pb-2 border-b border-gray-200" id="detailTitle">节点详情</h2>

                <div class="space-y-4">
                    <div>
                        <h3 class="font-medium text-gray-700 mb-1">基本信息</h3>
                        <table class="w-full text-sm">
                            <tbody>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500 w-1/3">traceId</td>
                                <td class="py-1.5 font-mono" id="detailTraceId"></td>
                            </tr>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500">spanId</td>
                                <td class="py-1.5 font-mono" id="detailSpanId"></td>
                            </tr>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500">父spanId</td>
                                <td class="py-1.5 font-mono" id="detailParentSpanId"></td>
                            </tr>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500">线程</td>
                                <td class="py-1.5" id="detailThreadName"></td>
                            </tr>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500">深度</td>
                                <td class="py-1.5" id="detailDepth"></td>
                            </tr>
                            </tbody>
                        </table>
                    </div>

                    <div>
                        <h3 class="font-medium text-gray-700 mb-1">时间信息</h3>
                        <table class="w-full text-sm">
                            <tbody>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500 w-1/3">开始时间</td>
                                <td class="py-1.5" id="detailStartTime"></td>
                            </tr>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500">结束时间</td>
                                <td class="py-1.5" id="detailEndTime"></td>
                            </tr>
                            <tr>
                                <td class="py-1.5 text-gray-500">耗时</td>
                                <td class="py-1.5 font-medium" id="detailCostTime"></td>
                            </tr>
                            </tbody>
                        </table>
                    </div>

                    <div id="methodInfo" class="hidden">
                        <h3 class="font-medium text-gray-700 mb-1">方法信息</h3>
                        <table class="w-full text-sm">
                            <tbody>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500 w-1/3">服务类名</td>
                                <td class="py-1.5 font-mono" id="detailServiceName"></td>
                            </tr>
                            <tr>
                                <td class="py-1.5 text-gray-500">方法名</td>
                                <td class="py-1.5 font-mono" id="detailMethodName"></td>
                            </tr>
                            </tbody>
                        </table>
                    </div>

                    <div id="sqlInfo" class="hidden">
                        <h3 class="font-medium text-gray-700 mb-1">SQL信息</h3>
                        <table class="w-full text-sm">
                            <tbody>
                            <tr class="border-b border-gray-100">
                                <td class="py-1.5 text-gray-500 w-1/3">SQL ID</td>
                                <td class="py-1.5 font-mono text-sm" id="detailSqlId"></td>
                            </tr>
                            </tbody>
                        </table>

                        <div class="mt-3">
                            <h4 class="font-medium text-gray-700 mb-2">格式化SQL</h4>
                            <pre class="bg-gray-800 text-gray-100 p-3 rounded-md text-xs overflow-x-auto font-mono" id="formattedSql"></pre>
                        </div>
                    </div>

                    <div class="flex items-center gap-2">
                        <span id="statusBadge" class="px-2 py-0.5 rounded-full text-xs font-medium"></span>
                    </div>
                </div>
            </div>

            <div id="noSelection" class="details-panel h-[70vh] flex flex-col items-center justify-center text-gray-500">
                <i class="fa fa-info-circle text-5xl mb-4 text-gray-300"></i>
                <p>请点击或悬停在节点上查看详情</p>
            </div>
        </div>
    </div>
</div>

<script>
    // 模拟TraceNode数据
    const mockTraceData = {
        "children": [
            {
                "children": [
                    {
                        "children": [
                            {
                                "children": [],
                                "costTime": 14,
                                "depth": 4,
                                "endTime": 1758002924069,
                                "fullSql": "SELECT  COLId,COLCustomerSerialid,COLOperateType,COLLogType,COLTCNo,COLOperatId,COLOperator,COLCreateDate,COLOperation,COLOperationForAdmin,COLDisposeDate,COLOrderStatusBefore,COLOrderStatusAfter  FROM CruiseOrderLog \n \n WHERE (COLLogType IN (54,60) AND COLOperatId = 5798 AND COLCustomerSerialid = '111661004')",
                                "methodName": "",
                                "originalSql": "SELECT COLId,COLCustomerSerialid,COLOperateType,COLLogType,COLTCNo,COLOperatId,COLOperator,COLCreateDate,COLOperation,COLOperationForAdmin,COLDisposeDate,COLOrderStatusBefore,COLOrderStatusAfter FROM CruiseOrderLog WHERE (COLLogType IN (?,?) AND COLOperatId = ? AND COLCustomerSerialid = ?)",
                                "parentSpanId": "097266b67db24215a103939af7a19ec2",
                                "path": "",
                                "serviceName": "",
                                "spanId": "e5ff9186ca5e4f669126fdd7bcc80694",
                                "sqlId": "com.holiday.order.platform.dao.mapper.tccruiseslog.CruiseOrderLogMapper.selectList",
                                "startTime": 1758002924055,
                                "success": true,
                                "threadName": "http-nio-8200-exec-6",
                                "traceId": "2838f589a6464dcd9dcc23e6f0abe8e7"
                            }
                        ],
                        "costTime": 17,
                        "depth": 3,
                        "endTime": 1758002924069,
                        "methodName": "selectList",
                        "parentSpanId": "69015958a75c4980a8c050a6376a794a",
                        "path": "CruiseOrderLogController#queryOrderLogList ==> CruiseOrderLogService#queryOrderLogList ==> CruiseOrderLogDao#getOrderLogList",
                        "serviceName": "$Proxy353",
                        "spanId": "097266b67db24215a103939af7a19ec2",
                        "startTime": 1758002924052,
                        "success": true,
                        "threadName": "http-nio-8200-exec-6",
                        "traceId": "2838f589a6464dcd9dcc23e6f0abe8e7"
                    }
                ],
                "costTime": 17,
                "depth": 2,
                "endTime": 1758002924069,
                "methodName": "getOrderLogList",
                "parentSpanId": "c7e452a7ea1c45279d48df87ff6fd5fc",
                "path": "CruiseOrderLogController#queryOrderLogList ==> CruiseOrderLogService#queryOrderLogList ==> CruiseOrderLogDao#getOrderLogList",
                "serviceName": "CruiseOrderLogDao",
                "spanId": "69015958a75c4980a8c050a6376a794a",
                "startTime": 1758002924052,
                "success": true,
                "threadName": "http-nio-8200-exec-6",
                "traceId": "2838f589a6464dcd9dcc23e6f0abe8e7"
            }
        ],
        "costTime": 27,
        "depth": 1,
        "endTime": 1758002924069,
        "methodName": "queryOrderLogList",
        "parentSpanId": "",
        "path": "CruiseOrderLogController#queryOrderLogList",
        "serviceName": "CruiseOrderLogController",
        "spanId": "c7e452a7ea1c45279d48df87ff6fd5fc",
        "startTime": 1758002924042,
        "success": true,
        "threadName": "http-nio-8200-exec-6",
        "traceId": "2838f589a6464dcd9dcc23e6f0abe8e7"
    };

    // 格式化时间戳为可读时间
    function formatTimestamp(timestamp) {
        const date = new Date(timestamp);
        return date.toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            fractionalSecondDigits: 3
        });
    }

    // 格式化SQL
    function formatSql(sql) {
        if (!sql) return "";
        try {
            return sqlFormatter.format(sql, {
                language: 'sql',
                indent: '  ',
                uppercase: true
            });
        } catch (e) {
            console.error("SQL格式化失败:", e);
            return sql;
        }
    }

    // 更新节点详情面板
    function updateNodeDetails(node) {
        const detailsPanel = document.getElementById('nodeDetails');
        const noSelection = document.getElementById('noSelection');

        detailsPanel.classList.remove('opacity-0', 'pointer-events-none');
        noSelection.classList.add('opacity-0', 'pointer-events-none');

        // 更新基本信息
        document.getElementById('detailTraceId').textContent = node.traceId || '-';
        document.getElementById('detailSpanId').textContent = node.spanId || '-';
        document.getElementById('detailParentSpanId').textContent = node.parentSpanId || '-';
        document.getElementById('detailThreadName').textContent = node.threadName || '-';
        document.getElementById('detailDepth').textContent = node.depth;

        // 更新时间信息
        document.getElementById('detailStartTime').textContent = formatTimestamp(node.startTime);
        document.getElementById('detailEndTime').textContent = formatTimestamp(node.endTime);

        // 格式化耗时显示
        let costTimeHtml = `${node.costTime} ms`;
        if (node.costTime > 1000) {
            costTimeHtml += ` (${(node.costTime / 1000).toFixed(2)} s)`;
        }
        document.getElementById('detailCostTime').textContent = costTimeHtml;

        // 更新状态徽章
        const statusBadge = document.getElementById('statusBadge');
        if (node.success) {
            statusBadge.textContent = '执行成功';
            statusBadge.className = 'px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800';
        } else {
            statusBadge.textContent = '执行失败';
            statusBadge.className = 'px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800';
        }

        // 更新标题
        document.getElementById('detailTitle').textContent = node.path || '节点详情';

        // 显示方法信息或SQL信息
        const methodInfo = document.getElementById('methodInfo');
        const sqlInfo = document.getElementById('sqlInfo');

        if (node.sqlId) {
            // SQL节点
            methodInfo.classList.add('hidden');
            sqlInfo.classList.remove('hidden');

            document.getElementById('detailSqlId').textContent = node.sqlId;
            document.getElementById('formattedSql').textContent = formatSql(node.fullSql || node.originalSql);
        } else {
            // 方法节点
            sqlInfo.classList.add('hidden');
            methodInfo.classList.remove('hidden');

            document.getElementById('detailServiceName').textContent = node.serviceName || '-';
            document.getElementById('detailMethodName').textContent = node.methodName || '-';
        }
    }

    // 初始化D3树形图
    function initTreeVisualization(data) {
        try {
            // 隐藏加载指示器
            document.getElementById('loadingIndicator').classList.add('opacity-0', 'pointer-events-none');
            setTimeout(() => {
                document.getElementById('loadingIndicator').style.display = 'none';
            }, 300);

            // 设置SVG尺寸和边距
            const svg = d3.select("#treeSvg");
            const container = svg.node().parentElement;
            const width = container.clientWidth;
            const height = container.clientHeight;
            const margin = { top: 40, right: 40, bottom: 40, left: 250 };
            const treeWidth = width - margin.left - margin.right;
            const treeHeight = height - margin.top - margin.bottom;

            // 创建缩放行为
            const zoom = d3.zoom()
                .scaleExtent([0.3, 3])
                .on("zoom", (event) => {
                    g.attr("transform", event.transform);
                });

            svg.call(zoom);

            // 创建容器并应用初始变换
            const g = svg.append("g")
                .attr("transform", `translate(${margin.left},${margin.top})`);

            // 定义树形布局
            const tree = d3.tree()
                .size([treeHeight, treeWidth]);

            // 将数据转换为层次结构
            const root = d3.hierarchy(data);

            // 计算节点位置
            tree(root);

            // 定义连接线生成器
            const link = d3.linkHorizontal()
                .x(d => d.y)
                .y(d => d.x);

            // 绘制连接线
            g.selectAll(".link")
                .data(root.links())
                .enter()
                .append("path")
                .attr("class", "link")
                .attr("fill", "none")
                .attr("stroke", "#999")
                .attr("stroke-width", 2)
                .attr("stroke-opacity", 0.7)
                .attr("d", link)
                .attr("stroke-dasharray", d => {
                    return !d.source.data.success || !d.target.data.success ? "4,4" : "none";
                });

            // 创建节点组
            const node = g.selectAll(".node")
                .data(root.descendants())
                .enter()
                .append("g")
                .attr("class", "node")
                .attr("transform", d => `translate(${d.y},${d.x})`)
                .on("click", toggleNode)
                .on("mouseover", function(event, d) {
                    d3.select(this).select("circle").classed("node-hover", true);
                    highlightRelatedLinks(d);
                    updateNodeDetails(d.data);
                })
                .on("mouseout", function() {
                    d3.select(this).select("circle").classed("node-hover", false);
                    resetLinkHighlight();
                });

            // 为每个节点添加圆形
            node.append("circle")
                .attr("r", d => d.data.sqlId ? 8 : 6)
                .attr("fill", d => {
                    if (!d.data.success) return "#EF4444";
                    return d.data.sqlId ? "#8B5CF6" : "#3B82F6";
                })
                .attr("stroke", "#fff")
                .attr("stroke-width", 1.5)
                .attr("cursor", "pointer");

            // 为每个节点添加文本标签
            node.append("text")
                .attr("dy", 0.35)
                .attr("x", d => d.children || d._children ? -12 : 12)
                .attr("text-anchor", d => d.children || d._children ? "end" : "start")
                .attr("font-size", 12)
                .attr("fill", "#333")
                .text(d => {
                    if (d.data.sqlId) {
                        const sqlIdParts = d.data.sqlId.split('.');
                        return sqlIdParts[sqlIdParts.length - 1] || 'SQL';
                    } else {
                        return d.data.methodName || 'Unknown';
                    }
                })
                .append("tspan")
                .attr("x", d => d.children || d._children ? -12 : 12)
                .attr("dy", "1.2em")
                .attr("font-size", 10)
                .attr("fill", "#666")
                .text(d => `${d.data.costTime}ms`);

            // 初始展开所有节点（简化逻辑，确保能看到树）
            root.descendants().forEach(d => {
                d._children = d.children;
                // 只折叠深度大于2的节点
                if (d.depth <= 2) {
                    d.children = d._children;
                } else {
                    d.children = null;
                }
            });

            // 高亮相关的连接线
            function highlightRelatedLinks(node) {
                resetLinkHighlight();

                // 高亮当前节点与其父节点的连接线
                if (node.parent) {
                    g.selectAll(".link")
                        .filter(d => d.source === node.parent && d.target === node)
                        .classed("link-highlight", true);
                }

                // 高亮当前节点与其子节点的连接线
                if (node.children || node._children) {
                    g.selectAll(".link")
                        .filter(d => d.source === node)
                        .classed("link-highlight", true);
                }
            }

            // 重置连接线高亮
            function resetLinkHighlight() {
                g.selectAll(".link")
                    .classed("link-highlight", false);
            }

            // 更新树布局
            function update(source) {
                // 重新计算布局
                tree(root);

                // 计算节点和连接线的过渡
                const nodes = root.descendants().reverse();
                const links = root.links();

                // 标准化节点的y坐标
                nodes.forEach(d => { d.y = d.depth * 180; });

                // 更新节点
                const node = g.selectAll(".node")
                    .data(nodes, d => d.id || (d.id = ++nodeId));

                // 创建新节点
                const nodeEnter = node.enter().append("g")
                    .attr("class", "node")
                    .attr("transform", d => `translate(${source.y},${source.x})`)
                    .on("click", toggleNode)
                    .on("mouseover", function(event, d) {
                        d3.select(this).select("circle").classed("node-hover", true);
                        highlightRelatedLinks(d);
                        updateNodeDetails(d.data);
                    })
                    .on("mouseout", function() {
                        d3.select(this).select("circle").classed("node-hover", false);
                        resetLinkHighlight();
                    });

                // 添加新节点的圆形
                nodeEnter.append("circle")
                    .attr("r", 0)
                    .attr("fill", d => {
                        if (!d.data.success) return "#EF4444";
                        return d.data.sqlId ? "#8B5CF6" : "#3B82F6";
                    })
                    .attr("stroke", "#fff")
                    .attr("stroke-width", 1.5)
                    .attr("cursor", "pointer")
                    .transition()
                    .duration(300)
                    .attr("r", d => d.data.sqlId ? 8 : 6);

                // 添加新节点的文本
                nodeEnter.append("text")
                    .attr("dy", 0.35)
                    .attr("x", d => d.children || d._children ? -12 : 12)
                    .attr("text-anchor", d => d.children || d._children ? "end" : "start")
                    .attr("font-size", 12)
                    .attr("fill", "#333")
                    .text(d => {
                        if (d.data.sqlId) {
                            const sqlIdParts = d.data.sqlId.split('.');
                            return sqlIdParts[sqlIdParts.length - 1] || 'SQL';
                        } else {
                            return d.data.methodName || 'Unknown';
                        }
                    })
                    .attr("opacity", 0)
                    .transition()
                    .duration(300)
                    .attr("opacity", 1);

                // 添加耗时信息
                nodeEnter.append("text")
                    .attr("dy", "1.5em")
                    .attr("x", d => d.children || d._children ? -12 : 12)
                    .attr("text-anchor", d => d.children || d._children ? "end" : "start")
                    .attr("font-size", 10)
                    .attr("fill", "#666")
                    .text(d => `${d.data.costTime}ms`)
                    .attr("opacity", 0)
                    .transition()
                    .duration(300)
                    .attr("opacity", 1);

                // 更新现有节点
                const nodeUpdate = nodeEnter.merge(node)
                    .transition()
                    .duration(300)
                    .attr("transform", d => `translate(${d.y},${d.x})`);

                // 移除离开的节点
                const nodeExit = node.exit()
                    .transition()
                    .duration(300)
                    .attr("transform", d => `translate(${source.y},${source.x})`)
                    .remove();

                nodeExit.select("circle")
                    .attr("r", 0);

                nodeExit.select("text")
                    .attr("opacity", 0);

                // 更新连接线
                const linkUpdate = g.selectAll(".link")
                    .data(links, d => d.target.id);

                // 添加新的连接线
                linkUpdate.enter().insert("path", "g")
                    .attr("class", "link")
                    .attr("fill", "none")
                    .attr("stroke", "#999")
                    .attr("stroke-width", 2)
                    .attr("stroke-opacity", 0.7)
                    .attr("stroke-dasharray", d => {
                        return !d.source.data.success || !d.target.data.success ? "4,4" : "none";
                    })
                    .attr("d", d => {
                        const o = {x: source.x, y: source.y};
                        return link({source: o, target: o});
                    })
                    .transition()
                    .duration(300)
                    .attr("d", link);

                // 更新现有连接线
                linkUpdate.merge(linkUpdate)
                    .transition()
                    .duration(300)
                    .attr("d", link);

                // 移除离开的连接线
                linkUpdate.exit()
                    .transition()
                    .duration(300)
                    .attr("d", d => {
                        const o = {x: source.x, y: source.y};
                        return link({source: o, target: o});
                    })
                    .remove();

                // 保存节点位置用于下次更新
                nodes.forEach(d => {
                    d.x0 = d.x;
                    d.y0 = d.y;
                });
            }

            // 节点展开/折叠切换
            function toggleNode(event, d) {
                event.stopPropagation();

                if (d.children) {
                    d._children = d.children;
                    d.children = null;
                } else {
                    d.children = d._children;
                    d._children = null;
                }

                update(d);
                updateNodeDetails(d.data);
                highlightRelatedLinks(d);
            }

            // 初始化节点ID
            let nodeId = 0;
            root.descendants().forEach(d => d.id = ++nodeId);

            // 保存初始位置
            root.x0 = treeHeight / 2;
            root.y0 = 0;

            // 初始化更新
            update(root);

            // 绑定缩放控制按钮事件
            document.getElementById('zoomIn').addEventListener('click', () => {
                svg.transition().call(zoom.scaleBy, 1.3);
            });

            document.getElementById('zoomOut').addEventListener('click', () => {
                svg.transition().call(zoom.scaleBy, 0.7);
            });

            document.getElementById('resetView').addEventListener('click', () => {
                svg.transition().call(
                    zoom.transform,
                    d3.zoomIdentity.translate(margin.left, margin.top)
                );
            });

        } catch (error) {
            console.error("树形图初始化失败:", error);
            // 显示错误信息
            document.getElementById('loadingIndicator').style.display = 'none';
            document.getElementById('errorMessage').classList.remove('hidden');
        }
    }

    // 页面加载完成后初始化
    document.addEventListener('DOMContentLoaded', () => {
        try {
            // 模拟API请求延迟
            setTimeout(() => {
                // 检查数据格式是否正确
                if (mockTraceData && typeof mockTraceData === 'object') {
                    initTreeVisualization(mockTraceData);
                } else {
                    throw new Error("无效的数据格式");
                }
            }, 800);
        } catch (error) {
            console.error("页面初始化失败:", error);
            document.getElementById('loadingIndicator').style.display = 'none';
            document.getElementById('errorMessage').classList.remove('hidden');
        }
    });
</script>
</body>
</html>
